﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq.Expressions;
using ExpressionEvaluator;
using System.Diagnostics;

namespace ExpressionEvalTest
{
	[TestClass]
	public class ExpressionTreeBuilderTest
	{

		#region Additional test attributes
		//
		// You can use the following additional attributes as you write your tests:
		//
		// Use ClassInitialize to run code before running the first test in the class
		// [ClassInitialize()]
		// public static void MyClassInitialize(TestContext testContext) { }
		//
		// Use ClassCleanup to run code after all tests in a class have run
		// [ClassCleanup()]
		// public static void MyClassCleanup() { }
		//
		// Use TestInitialize to run code before running each test 
		// [TestInitialize()]
		// public void MyTestInitialize() { }
		//
		// Use TestCleanup to run code after each test has run
		// [TestCleanup()]
		// public void MyTestCleanup() { }
		//
		#endregion

		[TestMethod]
		public void SimpleLambda()
		{
			Expression<Func<int, int>> lambda = new ExpressionEval("Convert.ToInt32(String.Concat(\"3\", x.ToString()))")
				.AddParam<int>("x")
				.AddLookupAssembly("ExpressionEvalTest")
				.AddLookupNamespace("ExpressionEvalTest", "ExpressionEvalTest")
				.Eval<Func<int, int>>();
			int result = lambda.Compile().Invoke(1);
			Assert.AreEqual(31, result);
		}

		[TestMethod]
		public void Operations()
		{
			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 * 2, "5 * 2", null, null);
			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 / 2, "5 / 2", null, null);
			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 % 2, "5 % 2", null, null);

			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 + 2, "5 + 2", null, null);
			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 - 2, "5 - 2", null, null);

			CompareLambdaWithExpressionEval<Func<bool>>(
				() => 5 < 2, "5 < 2", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => 5 > 2, "5 > 2", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => 5 >= 2, "5 >= 2", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => 5 <= 2, "5 <= 2", null, null);

			CompareLambdaWithExpressionEval<Func<bool>>(
				() => 5 == 2, "5 == 2", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => 5 != 2, "5 != 2", null, null);

			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 & 2, "5 & 2", null, null);
			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 ^ 2, "5 ^ 2", null, null);
			CompareLambdaWithExpressionEval<Func<int>>(
				() => 5 | 2, "5 | 2", null, null);

			CompareLambdaWithExpressionEval<Func<bool>>(
				() => (5 > 2) && (5 < 2), "(5 > 2) && (5 < 2)", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => (5 > 2) || (5 < 2), "(5 > 2) || (5 < 2)", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => (1 != 1) == true, "(1!=1) == true", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => (1 != 1) == false, "(1!=1) == false", null, null);
		}

		public class Test
		{
			public string TestProperty
			{
				get;
				set;
			}
		}
		[TestMethod]
		public void Parameters()
		{

			CompareLambdaWithExpressionEval<Func<int, int>>(
				(x) => x, "x", new object[] { 3 },
				Expression.Parameter(typeof(Int32), "x"));
			CompareLambdaWithExpressionEval<Func<int, int>>(
				(x) => 5 * x, "5 * x", new object[] { 3 },
				Expression.Parameter(typeof(Int32), "x"));
			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => y * x, "y * x", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			var testParameter = new Test
			{
				TestProperty = "test"
			};
			CompareLambdaWithExpressionEval<Func<Test, bool>>(
				(x) => x.TestProperty.Contains("t"), "x.TestProperty.Contains(\"t\")", new object[] { testParameter },
				Expression.Parameter(typeof(Test), "x"));
		}

		[TestMethod]
		public void TestExpressionExecutingOrder()
		{
			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => 1 + y * x + 4, "1 + y * x + 4", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => 1 + y * x / 2 + 4 / 2 - 6 % x * 3, "1 + y * x / 2 + 4 / 2 - 6 % x * 3", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => x / y / 2 % 1 + y * x / 2 + 4 / 2 - 6 % x * 3, "x / y / 2 % 1 + y * x / 2 + 4 / 2 - 6 % x * 3", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));
		}

		[TestMethod]
		public void TestExpressionExecutingOrderWithBracket()
		{
			CompareLambdaWithExpressionEval<Func<int, int>>(
				(x) => ((x)), "((x))", new object[] { 3 },
				Expression.Parameter(typeof(Int32), "x"));
			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => ((1 + y) * x / 2 + 4) / 2 - 6 % (x * (3 + y)), "((1 + y) * x / 2 + 4) / 2 - 6 % (x * (3 + y))", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => (1 + y) * x + 4, "(1 + y) * x + 4", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => (1 + (y * x)) / ((2 + 4) / 2 - 6 % x * 3), "(1 + (y * x)) / ((2 + 4) / 2 - 6 % x * 3)", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => x / (y / 2) % ((1 + y) * x / 2 + 4 / 2 - 6) % (x * 3), "(x / (y / 2) % ((1 + y) * x / 2 + 4 / 2 - 6) % (x * 3))", new object[] { 3, 5 },
				Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));
		}

		[TestMethod]
		public void Text()
		{
			CompareLambdaWithExpressionEval<Func<string>>(
				() => " test", "\" test\"", null, null);

			CompareLambdaWithExpressionEval<Func<int, string>>(
				(x) => x.ToString() + " test", "x.ToString() + \" test\"", new object[] { 3 },
				Expression.Parameter(typeof(Int32), "x"));
			CompareLambdaWithExpressionEval<Func<int, string>>(
				(x) => x.ToString().ToString() + " test", "x.ToString().ToString() + \" test\"", new object[] { 3 },
				Expression.Parameter(typeof(Int32), "x"));
			CompareLambdaWithExpressionEval<Func<int, int, int>>(
			    (x, y) => ((1 + y) * x / Convert.ToInt32("2") + 4) / 2 - 6 % (x * (3 + y)), "((1 + y) * x / Convert.ToInt32(\"2\") + 4) / 2 - 6 % (x * (3 + y))", new object[] { 3, 5 },
			    Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));

			CompareLambdaWithExpressionEval<Func<int, int, int>>(
				(x, y) => (Convert.ToInt32("2") + (y * x)) / ((2 + 4) / 2 - 6 % x * Convert.ToInt32(string.Concat("3", "1"))), "(Convert.ToInt32(\"2\") + (y * x)) / ((2 + 4) / 2 - 6 % x * Convert.ToInt32(String.Concat(\"3\", \"1\")))", new object[] { 3, 5 },
			    Expression.Parameter(typeof(Int32), "x"), Expression.Parameter(typeof(Int32), "y"));
		}

		[TestMethod]
		public void Indexers()
		{
			CompareLambdaWithExpressionEval<Func<string[], string>>(
				(x) => x[0], "x[0]", new object[] {new string[] { "test" }}, Expression.Parameter(typeof(string[]), "x"));
			CompareLambdaWithExpressionEval<Func<string[], string>>(
				(x) => x[0] + x[1], "x[0] + x[1]", new object[] { new string[] { "test", " other" } }, Expression.Parameter(typeof(string[]), "x"));
			CompareLambdaWithExpressionEval<Func<string[], int, string>>(
				(x, y) => x[0].Length.ToString() + x[1].ToLower() + y.ToString(), "x[0].Length.ToString() + x[1].ToLower() + y.ToString()", new object[] { new string[] { "test", " other" }, 2 }, Expression.Parameter(typeof(string[]), "x"), Expression.Parameter(typeof(int), "y"));
			CompareLambdaWithExpressionEval<Func<string[], int, string>>(
				(x, y) => ((x[0].Length.ToString() + x[1]) + y.ToString()), "((x[0].Length.ToString() + x[1]) + y.ToString())", new object[] { new string[] { "test", " other" }, 2 }, Expression.Parameter(typeof(string[]), "x"), Expression.Parameter(typeof(int), "y"));
		}

		[TestMethod]
		public void NotOperator()
		{
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => !(1 == 1), "!(1 == 1)", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => (5 > 2) || !(5 < 2), "(5 > 2) || !(5 < 2)", null, null);
		}
		[TestMethod]
		public void MinusSign()
		{
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => !(1 == -1), "!(1 == -1)", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => -1 == -1, "-1 == -1", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => !(-1 == -1), "!(-1 == -1)", null, null);
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => !(-1 == -1-2), "!(-1 == -1-2)", null, null);
			CompareLambdaWithExpressionEval<Func<int>>(
				() => ((-1 - 1) - 2), "((-1 -1) - 2)", null, null);
		}
		[TestMethod]
		public void Enum()
		{
			CompareLambdaWithExpressionEval<Func<TestEnum>>(
				() => TestEnum.FirstItem, "ExpressionEvalTest.TestEnum.FirstItem", null, null);
		}
		[TestMethod]
		public void Function()
		{
			CompareLambdaWithExpressionEval<Func<LambdaTest, int, int, int>>(
				(item, x, y) => item.FunctionTest(1 + x, x * (y + 1)), "item.FunctionTest(1+x, x*(y+1))", new object[] { new LambdaTest(), 3, 7 },
				Expression.Parameter(typeof(LambdaTest), "item"),
				Expression.Parameter(typeof(int), "x"),
				Expression.Parameter(typeof(int), "y")
				);
			CompareLambdaWithExpressionEval<Func<int, string>>(
				(x) => new DateTime(2009, 1, 1 + x).ToString("d"), "new DateTime(2009, 1, 1+x).ToString(\"d\")", new object[] { 1 },
				Expression.Parameter(typeof(int), "x"));
		}
		[TestMethod]
		public void NewInstance()
		{

			CompareLambdaWithExpressionEval<Func<Guid>>(
				() => new Guid(), "new Guid()", null, null);
			CompareLambdaWithExpressionEval<Func<DateTime>>(
				() => new DateTime(2009, 1, 1), "new DateTime(2009, 1, 1)", null, null);
			CompareLambdaWithExpressionEval<Func<string>>(
				() => new DateTime(2009, 1, 1).ToString("d"), "new DateTime(2009, 1, 1).ToString(\"d\")", null, null);			
			CompareLambdaWithExpressionEval<Func<string>>(
				() => "test" + new DateTime(2009, 1, 1).Year.ToString(), "\"test\" + new DateTime(2009, 1, 1).Year.ToString()", null, null);
		}

		[TestMethod]
		public void Nullable()
		{
			CompareLambdaWithExpressionEval<Func<string, bool>>(
				(x) => x == null, "x == null", new object[] { null },
				Expression.Parameter(typeof(string), "x"));
		}

		[TestMethod]
		public void Lambda()
		{
			CompareLambdaWithExpressionEval<Func<LambdaTest, bool>>(
				(x) => x.Test(val => val == 1), "x.Test(val => val == 1)", new object[] { new LambdaTest() },
				Expression.Parameter(typeof(LambdaTest), "x"));
			CompareLambdaWithExpressionEval<Func<bool>>(
				() => new LambdaTest().Test(val => val == 1), "new ExpressionEvalTest.LambdaTest().Test(val => val == 1)", null, null);

			CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
				(list) => list.Where(item => item.StartsWith("test")).ToList(), 
				"list.Where(item => item.StartsWith(\"test\")).ToList()", 
				new object[] {new List<string> { "test", "test2", "failtest" }},
				Expression.Parameter(typeof(List<string>), "list"));

			CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
				(list) => list.AsQueryable()
					.Where(item => item.StartsWith("test"))
					.OrderBy(item=>item.Length).ToList(),
				@"list.AsQueryable()
					.Where(item => item.StartsWith(""test""))
					.OrderBy(item=>item.Length)
					.ToList()",
				new object[] { new List<string> { "test", "test2", "failtest" } },
				Expression.Parameter(typeof(List<string>), "list"));

			CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
				(list) => list.AsQueryable()
					.Skip(1).Take(1)
					.OrderBy(item => item.Length).ToList(),
				@"list.AsQueryable()
					.Skip(1).Take(1)
					.OrderBy(item=>item.Length)
					.ToList()",
				new object[] { new List<string> { "test", "test2", "failtest" } },
				Expression.Parameter(typeof(List<string>), "list"));
		}

        [TestMethod]
        public void Anonymous_types_serialized_type()
        {
            CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
                (list) => list.AsQueryable()
                    .Select(item => new { Text = item.ToString()})
                    .Select(anonymous => anonymous.Text)
                    .ToList(),
                @"list.AsQueryable()
                    .Select(item => new <>f__AnonymousType0`2(Text = item.ToString()))
                    .Select(anonymous => anonymous.Text)
                    .ToList()",
                new object[] { new List<string> { "test", "test2", "failtest" } },
                Expression.Parameter(typeof(List<string>), "list"));
            CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
                (list) => list.AsQueryable()
                    .Select(item => new { Text = item.ToString(), Length = item.Length })
                    .Select(anonymous => anonymous.Text + anonymous.Length.ToString())
                    .ToList(),
                @"list.AsQueryable()
                    .Select(item => new <>f__AnonymousType0`2(Text = item.ToString(), Length = item.Length))
                    .Select(anonymous => anonymous.Text + anonymous.Length.ToString())
                    .ToList()",
                new object[] { new List<string> { "test", "test2", "failtest" } },
                Expression.Parameter(typeof(List<string>), "list"));
        }

        [TestMethod]
        public void Anonymous_types_direct_type()
        {
            CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
                (list) => list.AsQueryable()
                    .Select(item => new { Text = item.ToString() })
                    .Select(anonymous => anonymous.Text)
                    .ToList(),
                @"list.AsQueryable()
                    .Select(item => new { Text = item.ToString() })
                    .Select(anonymous => anonymous.Text)
                    .ToList()",
                new object[] { new List<string> { "test", "test2", "failtest" } },
                Expression.Parameter(typeof(List<string>), "list"));
            CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
                (list) => list.AsQueryable()
                    .Select(item => new { Text = item.ToString(), Length = item.Length })
                    .Select(anonymous => anonymous.Text + anonymous.Length.ToString())
                    .ToList(),
                @"list.AsQueryable()
                    .Select(item => new { Text = item.ToString(), Length = item.Length })
                    .Select(anonymous => anonymous.Text + anonymous.Length.ToString())
                    .ToList()",
                new object[] { new List<string> { "test", "test2", "failtest" } },
                Expression.Parameter(typeof(List<string>), "list"));
            CompareLambdaWithExpressionEval<Func<List<string>, List<string>>>(
                (list) => list.AsQueryable()
                    .Select(item => new { Text = item.ToString(),
                                          Length = item.Length,
                                          Length1 = item.Length,
                                          Length2 = item.Length,
                                          Length3 = item.Length,
                                          Length4 = item.Length,
                                          Length5 = item.Length
                    })
                    .Select(anonymous => anonymous.Text + anonymous.Length.ToString())
                    .ToList(),
                @"list.AsQueryable()
                    .Select(item => new { Text = item.ToString(),
                                          Length = item.Length,
                                          Length1 = item.Length,
                                          Length2 = item.Length,
                                          Length3 = item.Length,
                                          Length4 = item.Length,
                                          Length5 = item.Length })
                    .Select(anonymous => anonymous.Text + anonymous.Length.ToString())
                    .ToList()",
                new object[] { new List<string> { "test", "test2", "failtest" } },
                Expression.Parameter(typeof(List<string>), "list"));
        }

		public static void CompareLambdaWithExpressionEval<TDelegate>(Expression<TDelegate> lambda, string code, object[] functionValues, params ParameterExpression[] parameters)
		{
			var Evaluator = new ExpressionEval(code);
			Evaluator.AddLookupAssembly("System.Core");
			Evaluator.AddLookupNamespace("ExpressionEvalTest");
			Evaluator.AddExtension("System.Linq", "IQueryable`1");
			Evaluator.AddExtension("System.Linq", "IEnumerable`1");
			if (parameters != null)
			{
				foreach (ParameterExpression param in parameters)
				{
					Evaluator.AddParam(param);
				}
			}

			LambdaExpression eval = Evaluator.Eval<TDelegate>();
			object lambdaResult = (lambda as LambdaExpression).Compile().DynamicInvoke(functionValues);
			object evalResult = eval.Compile().DynamicInvoke(functionValues);

			// if results are enumerable, then compare by content
			if ((lambdaResult is System.Collections.IEnumerable) && (evalResult is System.Collections.IEnumerable))
			{
				var lambdaE = lambdaResult as System.Collections.IEnumerable;
				var evalE = (evalResult as System.Collections.IEnumerable).GetEnumerator();
				evalE.Reset();

				foreach (var item in lambdaE)
				{
					evalE.MoveNext();
					var evalItem = evalE.Current;
					Assert.AreEqual(item, evalItem,
				string.Format("Eval item: {0}\nLambda item: {1}", evalItem.ToString(), item.ToString()));
				}
			}
			else
			{
				Assert.AreEqual(lambdaResult, evalResult,
					string.Format("Code: {0}\nEval: {1}\nLambda: {2}", code, eval.ToString(), lambda.ToString()));
			}
			System.Diagnostics.Debug.WriteLine(string.Format("Code: {0}\nEval: {1}\nLambda: {2}", code, eval.ToString(), lambda.ToString()));

			TestEvalizer<TDelegate>(lambda, functionValues, parameters, lambdaResult);
		}

		private static void TestEvalizer<TDelegate>(Expression<TDelegate> lambda, object[] functionValues, ParameterExpression[] parameters, object lambdaResult)
		{
			string code = string.Empty;
			try
			{
				// test evalizer by serializing and deserializing lambda to text and back
				Evalizer evalizer = new Evalizer()
					.AddLookupNamespace("System.Linq")
					.AddLookupNamespace("System.Core")
					.AddLookupNamespace("ExpressionEvalTest");
				code = evalizer.Evalize(lambda);
				var EvalizerEvaluator = new ExpressionEval(code);
				EvalizerEvaluator.AddLookupAssembly("System.Core");
				EvalizerEvaluator.AddLookupNamespace("ExpressionEvalTest");
				EvalizerEvaluator.AddExtension("System.Linq", "IQueryable`1");
				EvalizerEvaluator.AddExtension("System.Linq", "IEnumerable`1");
				if (parameters != null)
				{
					foreach (ParameterExpression param in parameters)
					{
						EvalizerEvaluator.AddParam(param);
					}
				}
				LambdaExpression evalizerEval;

				evalizerEval = EvalizerEvaluator.Eval<TDelegate>();

				object evalizerResult = evalizerEval.Compile().DynamicInvoke(functionValues);
				if ((lambdaResult is System.Collections.IEnumerable) && (evalizerResult is System.Collections.IEnumerable))
				{
					var lambdaE = lambdaResult as System.Collections.IEnumerable;
					var evalE = (evalizerResult as System.Collections.IEnumerable).GetEnumerator();
					evalE.Reset();

					foreach (var item in lambdaE)
					{
						evalE.MoveNext();
						var evalItem = evalE.Current;
						Assert.AreEqual(item, evalItem,
					string.Format("Eval item: {0}\nLambda item: {1}", evalItem.ToString(), item.ToString()));
					}
				}
				else
				{
					Assert.AreEqual(lambdaResult, evalizerResult,
						string.Format("Code: {0}\nEval: {1}\nLambda: {2}", code, evalizerEval.ToString(), lambda.ToString()));
				}
			}
			catch (Exception e)
			{
				Trace.WriteLine(string.Format("Code: {0}; Lambda type:{1}; Exception:{2}",
					code, typeof(TDelegate).ToString(), e.ToString()));
				Debug.WriteLine(string.Format("Code: {0}; Lambda type:{1}; Exception:{2}",
					code, typeof(TDelegate).ToString(), e.ToString()));
				throw;
			}
		}


	}

	public class LambdaTest
	{
		public int FunctionTest(int x, int y)
		{
			return x + y;
		}
		public bool Test(Func<int, bool> lambda)
		{
			return lambda(1);
		}
	}
    public enum TestEnum
    {
        FirstItem,
        SecondItem,
        CommonItem,
        LastItem
    }
}
